// Copyright (c) 2019 Presto Labs Pte. Ltd.
// Author: xguo

#pragma once

#include <algorithm>
#include <cmath>
#include <cstdio>
#include <limits>
#include <memory>
#include <string>

#include "coin/proto/coin_order_enums.pb.h"
#include "coin2/base/config/app_config.h"
#include "coin2/base/log.h"
#include "coin2/base/numberic_util.h"
#include "coin2/exchange/base/symbology/currency.h"
#include "coin2/exchange/base/symbology/symbol.h"
#include "coin2/strategy/order_executor.pb.h"

const double TICKBP_COEF = 8.872;
const double TICKBP_EXP = 0.3294;

namespace coin2::exchange::base::symbology {

class TickPrice {
 public:
  virtual ~TickPrice() = default;
  virtual int max_precision() const = 0;

  virtual double Infer2bpBookMult(double price) const = 0;

  // price : 5 20 50 => down_ticksize=15, up_ticksize=30
  virtual double UpTicksize(double price) const = 0;
  virtual double DownTicksize(double price) const = 0;
  virtual bool CheckRoundedPrice(double price) const = 0;
  virtual double GetNthNextPrice(double price, int n_ticks) const = 0;
  virtual double GetNthPrevPrice(double price, int n_ticks) const = 0;
  virtual double GetNextPrice(double price) const = 0;
  virtual double GetPrevPrice(double price) const = 0;

  virtual double RoundUp(double price) const = 0;
  virtual double RoundDown(double price) const = 0;
  virtual double Round(double price) const = 0;
  virtual double RoundAggressively(double price, ::coin::proto::OrderDirection dir) const = 0;
  virtual double RoundPassively(double price, ::coin::proto::OrderDirection dir) const = 0;

  static double Ceil(double price, double tick_price) {
    constexpr double buffer = 1.e-6;
    return std::ceil(price / tick_price - buffer) * tick_price;
  }

  static double Floor(double price, double tick_price) {
    constexpr double buffer = 1.e-6;
    return std::floor(price / tick_price + buffer) * tick_price;
  }

  double RoundWithPolicy(
      double raw_price,
      ::coin::proto::OrderDirection dir,
      coin2::strategy::TickRounding policy) const {
    using coin2::strategy::TickRounding;
    switch (policy) {
      case TickRounding::TICK_ROUND_UNINIT:
        THROW();
        return 0.;
      case TickRounding::TICK_ROUND_EXACT:
        CHECK_THROW(CheckRoundedPrice(raw_price));
        return raw_price;
      case TickRounding::TICK_ROUND_OUTWARD:
        return RoundAggressively(raw_price, dir);
      case TickRounding::TICK_ROUND_INWARD:
        return RoundPassively(raw_price, dir);
      case TickRounding::TICK_ROUND_NEAREST:
        return Round(raw_price);

      case TickRounding::TICK_ROUND_FLOOR:
        return RoundDown(raw_price);
      case TickRounding::TICK_ROUND_CEIL:
        return RoundUp(raw_price);
      default:
        THROW();
    }
    return 0;
  }

  virtual std::string Format(double price) const = 0;
};

class FixedTickPrice : public TickPrice {
 public:
  explicit FixedTickPrice(double tick_size)
      : tick_size_(tick_size),
        precision_(impl::CalculatePrecision(tick_size)),
        format_str_(fmt::format("{{:.{}f}}", precision_)) {}

  double Infer2bpBookMult(double price) const {
    double adj_tickbp = TICKBP_COEF * std::pow(tick_size_ / price, TICKBP_EXP);
    if (adj_tickbp <= 0) return 0;
    return std::min(1 / adj_tickbp, 4.0);
  }

  int max_precision() const override { return precision_; }

  double UpTicksize(double /*price*/) const override { return tick_size_; }
  double DownTicksize(double /*price*/) const override { return tick_size_; }

  bool CheckRoundedPrice(double price) const override {
    return std::fabs(RoundDown(price + 1e-14) - price) < 1e-14;
  }

  double GetNthNextPrice(double price, int n_ticks) const override {
    // humm... let's move on for now.
    // Assumes price is valid price. Because of floating point error, it is hard
    // to implement is_valid_price function.
    return price + n_ticks * tick_size_;
  }

  double GetNthPrevPrice(double price, int n_ticks) const override {
    return price - n_ticks * tick_size_;
  }

  double GetNextPrice(double price) const override { return price + tick_size_; }

  double GetPrevPrice(double price) const override { return price - tick_size_; }

  double RoundUp(double price) const override { return Ceil(price, tick_size_); }

  double RoundDown(double price) const override { return Floor(price, tick_size_); }

  double Round(double price) const override { return std::round(price / tick_size_) * tick_size_; }

  double RoundAggressively(double price, coin::proto::OrderDirection dir) const override {
    using coin::proto::OrderDirection;
    if (dir == OrderDirection::DIRECTION_BUY) {
      return Ceil(price, tick_size_);
    }
    if (dir == OrderDirection::DIRECTION_SELL) {
      return Floor(price, tick_size_);
    }
    THROW() << "Unimplemented for OrderDir " << dir;
    return 0;
  }

  double RoundPassively(double price, coin::proto::OrderDirection dir) const override {
    using coin::proto::OrderDirection;
    if (dir == OrderDirection::DIRECTION_BUY) {
      return Floor(price, tick_size_);
    }
    if (dir == OrderDirection::DIRECTION_SELL) {
      return Ceil(price, tick_size_);
    }
    THROW() << "Unimplemented for OrderDir " << dir;
    return 0;
  }

  std::string Format(double price) const override { return fmt::format(format_str_, Round(price)); }

 private:
  const double tick_size_;
  const int precision_;
  const std::string format_str_;
};

class RangeVariableTickPrice : public TickPrice {
 public:
  explicit RangeVariableTickPrice(const coin::proto::ProductInfoProto& pi) : pi_(pi) {
    max_precision_ = -100000;
    for (const auto& tick : pi_.price_ticksize()) {
      double tick_size = tick.value();
      double precision = impl::CalculatePrecision(tick_size);
      if (precision > max_precision_) {
        max_precision_ = precision;
      }
    }
  }

  double Infer2bpBookMult(double price) const {
    double adj_tickbp = TICKBP_COEF * std::pow(GetTick(price) / price, TICKBP_EXP);
    if (adj_tickbp <= 0) return 0;
    return std::min(1 / adj_tickbp, 4.0);
  }

  double GetTick(double price_corrected) const {
    double tick_size = 0.;
    for (const auto& tick : pi_.price_ticksize()) {
      if (price_corrected >= tick.from() && price_corrected < tick.to()) {
        tick_size = tick.value();
        break;
      }
    }
    return tick_size;
  }

  int max_precision() const override { return max_precision_; }

  double RoundAggressively(double price, coin::proto::OrderDirection dir) const override {
    using coin::proto::OrderDirection;
    if (dir == OrderDirection::DIRECTION_BUY) {
      return Ceil(price, GetTick(price));
    }
    if (dir == OrderDirection::DIRECTION_SELL) {
      return Floor(price, GetTick(price));
    }
    THROW() << "Unimplemented for OrderDir " << dir;
    return 0;
  }

  double RoundPassively(double price, coin::proto::OrderDirection dir) const override {
    using coin::proto::OrderDirection;
    if (dir == OrderDirection::DIRECTION_BUY) {
      return Floor(price, GetTick(price));
    }
    if (dir == OrderDirection::DIRECTION_SELL) {
      return Ceil(price, GetTick(price));
    }
    THROW() << "Unimplemented for OrderDir " << dir;
    return 0;
  }

  double GetNextCorrectedTick(double price) const {
    return GetTick(price * RangeVariableTickPrice::CORRUPTION_POSITIVE);
  }

  double GetPrevCorrectedTick(double price) const {
    return GetTick(price * RangeVariableTickPrice::CORRUPTION_NEGATIVE);
  }

  double UpTicksize(double price) const override { 
    return GetNextPrice(price) - price; 
  }

  double DownTicksize(double price) const override {
    return price - GetPrevPrice(price);
  }

  bool CheckRoundedPrice(double price) const override {
    return std::fabs(RoundDown(price + 1e-13) - price) < 1e-13;
  }

  double RoundDown(double price) const override {
    double corrupted_price = price * RangeVariableTickPrice::CORRUPTION_POSITIVE;
    double tick_price = GetPrevCorrectedTick(corrupted_price);
    return Floor(corrupted_price, tick_price);
  }

  double RoundUp(double price) const override {
    double corrupted_price = price * RangeVariableTickPrice::CORRUPTION_NEGATIVE;
    double tick_price = GetNextCorrectedTick(corrupted_price);
    return Ceil(corrupted_price, tick_price);
  }

  double GetNextPrice(double price) const override {
    double floored = RoundDown(price);
    double tick_price = GetNextCorrectedTick(floored);
    return RoundDown(floored + tick_price);
  }

  double GetPrevPrice(double price) const override {
    double ceiled = RoundUp(price);
    double tick_price = GetPrevCorrectedTick(price);
    return RoundUp(ceiled - tick_price);
  }

  // TODO(xguo) fast impl
  double GetNthNextPrice(double price, int n_ticks) const override {
    CHECK(0 <= n_ticks && n_ticks <= 20) << "n_ticks out of normal range!";
    while (n_ticks > 0) {
      price = GetNextPrice(price);
      n_ticks--;
    }
    return price;
  }

  // TODO(xguo) fast impl
  double GetNthPrevPrice(double price, int n_ticks) const override {
    CHECK(0 <= n_ticks && n_ticks <= 20) << "n_ticks out of normal range!";
    while (n_ticks > 0) {
      price = GetPrevPrice(price);
      n_ticks--;
    }
    return price;
  }

  double Round(double price) const override {
    double floored = RoundDown(price);
    double ceiled = RoundUp(price);

    if (price - floored < ceiled - price) {
      return floored;
    }
    return ceiled;
  }

  std::string Format(double price) const override {
    int precision = impl::CalculatePrecision(GetTick(price));
    CHECK_GE(precision, 0) << precision;

    double rounded = Round(price);
    if (precision == 0) {
      return fmt::format("{}", static_cast<int>(rounded));
    } else {
      std::string str = fmt::format("{{:.{}f}}", precision);
      return fmt::format(str, rounded);
    }
    CHECK(false);
    return "";
  }

 private:
  static constexpr double CORRUPTION_POSITIVE = (1. + 1.e-10);
  static constexpr double CORRUPTION_NEGATIVE = (1. - 1.e-10);
  const coin::proto::ProductInfoProto pi_;
  int max_precision_;
};

}  // namespace coin2::exchange::base::symbology
